查看原文
其他

Kotlin的特性应用示例,原来还可以这么玩

Tears丶残阳 郭霖 2019-04-29



今日科技快讯


昨日,腾讯发布2018年全年财报:第四季度腾讯实现营收848.96亿元,同比增长28%,2018年全年实现总收入为3126.94亿元(455.61亿美元 ),同比增长32%;净利润774.69亿元(112.88亿美元),同比增长19%。


作者简介


又到了令人激动的周五了,提前祝大家周末愉快!

本篇文章来自 Tears丶残阳 的投稿,和大家分享了自己的Kotlin实践项目,希望对大家有所帮助!

Tears丶残阳博客地址:

https://blog.csdn.net/xiazunyang


正文


相信每一个做Android的程序猿都没少重写Activity的onActivityResult和onPermissionResult方法,随之而来的各种框架也是层出不穷。其中,我觉得这种使用Fragment来实现的方法是最好的。所以我思路也是这种,但是特点就是充分使用了Kotlin的各种特性,使逻辑清晰、层次分明,代码也更简洁。

在开始之前,最好先阅读这篇博客:

https://blog.csdn.net/gdutxiaoxu/article/details/86498647

以了解实现基本思路。

思路简介:

利用在Fragment中调用startActivityForResult方法后,会回调到同Fragment中onActivityResult方法、以及调用onRequestPermissionsResult方法后,会回调到同Fragment中onRequestPermissionsResult方法的特性,在调用startActivityForResult和requestPermission方法时,传入回调后保存起来,等onActivityResult和onRequestPermissionsResult方法调用时,通过requestCode取出回调并调用。

以上思路已经有基于Java的具体实现了,上面的博客中有源码,直接抄走就能用。

那么相比原生的写法,我们少干了什么呢

1.不用定义requestCode。

2.不用重写onActivityResult和onRequestPermissionsResult方法。

那么相比以上方法,使用Kotlin后还能有什么新的玩法吗?我先整理一下:

1.使用Kotlin的SAM特性,传入Lambda作为回调函数,写法简单。

2.Builder设计模式,链式调用设置其它回调,层次清晰。

3.使用Kotlin的扩展函数特性,直接在Activity或Fragment中调用。

4.使用Kotlin的inline特性和具名参数特性,仿Anko的写法,语法短小精悍。

接下来先用Kotlin将基本思路实现一下,共由四个主要部分组成:

一、Fragment的创建

private const val FRAGMENT_TAG = "EmptyFragment"

/**
 * 查找Activity中有没有EmptyFragment,如果没有则创建EmptyFragment并添加到Activity中
 * @receiver FragmentManager
 * @return EmptyFragment 已创建并已添加到Activity中的Fragment
 */
private fun findOrCreateEmptyFragment(manager: FragmentManager): EmptyFragment {
    return manager.findFragmentByTag(FRAGMENT_TAG) as? EmptyFragment ?: EmptyFragment().also {
        manager.beginTransaction().replace(android.R.id.content, it, FRAGMENT_TAG).commitNowAllowingStateLoss()
    }
}

这是一个私有的包级函数,在FragmentManager中,通过FRAGMENT_TAG来查找Fragment,如果找到了,则强转为EmptyFragment,如果没有找到,则创建一个新的,添加到FragmentManager后,返回EmptyFragment对象,此方法结束。

二、requestCode的创建

private val codeGenerator = Random()
private val resultHolder = LinkedHashMap<Int, LambdaHolder<Intent>>()
private val permissionHolder = LinkedHashMap<Int, LambdaHolder<Unit>>()

private fun <M : Map<Int, *>> codeGenerate(map: M): Int {
    var requestCode: Int
    do {
        requestCode = codeGenerator.nextInt(0xFFFF)
    } while (requestCode in map.keys)
    return requestCode
}

resultHolder为startActivityForResult的回调持有类,permissionHolder为requestPermission的回调持有类,都是一个Map集合,codeGenerate方法也是一个包级函数,参数接收以上两个Map之一,然后创建出一个与已有Key不同的新编码,然后返回,此方法结束。

以上,都是Java中有的实现。

三、构建者模式对象的定义

class LambdaHolder<T>(val onSuccess: (T) -> Unit) {
    private var onBefore: () -> Unit = {}
    private var onDefined: (T?) -> Unit = {}
    private var onCanceled: () -> Unit = {}
    private var onDenied: (List<String>) -> Unit = {}

    /**
     * 设置在startActivityForResult后,新Activity返回时,resultCode为CANCELED时的回调
     * @param callback () -> Unit
     * @return LambdaHolder<T>  返回自己,用于继续设置其它回调
     */
    fun setOnCanceledCallback(callback: () -> Unit): LambdaHolder<T> {
        onCanceled = callback
        return this
    }

    /**
     * 设置在requestPermission后,申请的权限被拒绝时的回调
     * @param callback (List<String>) -> Unit 向外提供被拒绝的权限列表
     * @return LambdaHolder<T>   返回自己,用于继续设置其它回调
     */
    fun setOnDeniedCallback(callback: (List<String>) -> Unit): LambdaHolder<T> {
        onDenied = callback
        return this
    }

    /**
     * 设置在onActivityResult后,调用其它回调之前执行的回调
     * @param callback () -> Unit
     * @return LambdaHolder<T>
     */
    fun setBeforeCallback(callback: () -> Unit): LambdaHolder<T> {
        onBefore = callback
        return this
    }

    /**
     * 设置在startActivityForResult后,新Activity返回时,resultCode为USER_DEFINED时的回调
     * @param callback (T?) -> Unit 用户自定义的操作可能会用到Intent对象
     * @return LambdaHolder<T>  返回自己,用于继续设置其它回调
     */
    fun setOnDefinedCallback(callback: (T?) -> Unit): LambdaHolder<T> {
        onDefined = callback
        return this
    }

    /**
     * 当申请权限被拒绝时,调用此方法
     * @param list List<String> 被拒绝的权限列表
     * @return Unit
     */
    fun onDenied(list: List<String>) = this.onDenied.invoke(list)

    /**
     * 当从新Activity返回时,resultCode为USER_DEFINED时调用此方法
     * @return Unit
     */
    fun onDefined(t: T?) = this.onDefined.invoke(t)

    /**
     * 当从新Activity返回时,先执行此回调
     * @return Unit
     */
    fun before() = this.onBefore.invoke()

    /**
     * 当从新Activity返回时,resultCode为CANDELED时调用此方法
     * @return Unit
     */
    fun onCanceled() = this.onCanceled.invoke()
}

这个类中,构造函数中接收一个完成、成功时的Lambda回调,有一个泛型参数,这个泛型主要是为了后续的扩展而存在的。另外还有4个私有的Lambda属性,它们都有一个默认的空实现,还可以分别通过setOnCanceledCallback、setOnDeniedCallback、setBeforeCallback以及setOnDefinedCallback方法来设置新的实现,并且设置后,还会将自己返回作为返回值。每个Lambda都有一个对外暴露用来调用回调的方法,它们会在适当的时机由EmptyFragment的onActivityResult和onRequestPermissionsResult方法来调用。

四、EmptyFragment的实现

internal class EmptyFragment : Fragment() {

    internal fun startActivityForResult(requestCode: Int, intent: Intent, options: Bundle? = null, callback: (Intent) -> Unit): LambdaHolder<Intent> {
        return LambdaHolder(callback).also {
            resultHolder[requestCode] = it
            startActivityForResult(intent, requestCode, options)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        //取出与requestCode对应的对象,然后执行与resultCode对应的回调
        resultHolder.remove(requestCode)?.let {
            it.before()
            when (resultCode) {
                FragmentActivity.RESULT_OK -> it.onSuccess(data ?: Intent())
                FragmentActivity.RESULT_CANCELED -> it.onCanceled()
                else -> it.onDefined(data)
            }
        }
    }

    internal fun requestPermissions(requestCode: Int, vararg permissions: String, onRequestDone: (Unit) -> Unit): LambdaHolder<Unit> {
        return LambdaHolder(onRequestDone).also {
            //如果系统版本大于Android6.0并且未授予此权限,则申请权限
            if (Build.VERSION.SDK_INT > 22 && !checkPermissions(*permissions)) {
                //将回调加入待调用Map存起来,然后申请权限
                permissionHolder[requestCode] = it
                requestPermissions(permissions, requestCode)
            } else {
                //否则当作申请成功处理
 
               it.onSuccess(Unit)
            }
        }
    }

    private fun checkPermissions(vararg permission: String): Boolean {
        return permission.all {
            ActivityCompat.checkSelfPermission(ctx, it) == PackageManager.PERMISSION_GRANTED
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        //取出与requestCode对应的回执记录,如果为空,则结束此方法
        val lambdaHolder = permissionHolder.remove(requestCode) ?: return
        //当有正在申请的权限未结束时,permissions和grantResults会是空的,此时为申请失败,做中断处理
        if (permissions.isEmpty() && grantResults.isEmpty()) return
        //将未授予的权限加入到一个列表中
        grantResults.toList().mapIndexedNotNull { index, result ->
            if (result != PackageManager.PERMISSION_GRANTED) permissions[index] else null
        }.let {
            //通过列表是否为空来判断权限是否授予,然后执行对应的回调
            if (it.isEmpty()) lambdaHolder.onSuccess(Unit) else lambdaHolder.onDenied(it)
        }
    }

}

此类中新定义了startActivityForResult方法和requestPermissions方法,接收的参数除了原生方法的几个参数以外,还多了一个Lambda类型的参数,此Lambda为成功时的回调。要注意的是,这两个方法都有一个LambdaHolder类型的返回值,暂时先不解释。

1.我们先来看startActivityForResult方法与onActivityResult方法,我们先是创建了一个LambdaHolder对象,将成功时的Lambda回调保存起来了,在返回LambdaHolder对象之前,我们将requestCode作为Key、LambdaHolder对象作为Value存入了resultHolder中,然后调用了Fragment中原生的startActivityForResult方法。直到启动的Activity返回参数后,onActivityResult方法会回调,此时,我们先通过requestCode取出并移除保存的LambdaHolder对象,然后通过区分resultCode来调用LambdaHolder中不同的回调方法。其中,before回调无论resultCode为何值时,都会优先其它回调先执行一次,这个回调用于忽略返回结果的情况下,执行某些动作。

2.再来看其它的几个方法,在申请权限之前,我们还是先创建了一个LambdaHolder对象,然后判断系统版本以及判断是否已经授予权限,只有当系统版本大于Android6.0并且未授予权限时,我们才继续申请权限,否则当作申请成功处理,直接调用成功时的回调Lambda即可。申请权限前,还是先将requestCode与LambdaHolder对象存入permissionHolder中保存起来,再调用原生的requestPermissions方法来申请权限。直到onRequestPermissionsResult方法回调时,我们先通过requestCode取出LambdaHolder对象,做一下判空处理,为空时我们自然无法处理接下来的事情,所以直接结束方法。再判断一下permissions与grantResults是否是空数组,因为在已经有权限在申请过程中又发起新的权限申请,就会导致这两个数组都是空的情况发生,此时我们也直接结束方法。之后我们通过mapIndexedNotNull高阶函数取出未授予的权限,通过列表是否为空来判断所有的权限是否已经授予,最后执行成功或失败的回调方法。

3.新定义的startActivityForResult方法和requestPermissions方法都有一个LambdaHolder类型的返回值,我们可以调LambdaHolder中的setOnCanceledCallback、setBeforeCallback、setOnDefinedCallback以及setOnDeniedCallback方法来继续设置其它回调。

五、使用Kotlin扩展函数封装

因为EmptyFragment只是模块只可调用(internal),其它方法都是私有的,所以还需要进一步封装,将所有步骤连起来。添加以下两个包级函数。

/**
 * 启动Activity并接收Intent的扩展方法,接收回调时不需要重写[Activity#onActivityResult]方法
 * @receiver F  基于[FragmentActivity]的扩展方法
 * @param intent Intent [#startActivity]必需的参数
 * @param options Bundle?   动画参数
 * @param callback (data: Intent) -> Unit   返回此界面时,当[#resultCode]为 RESULT_OK时的回调
 * @return LambdaHolder<Intent> 可以在此对象上继续调用 [LambdaHolder#onCanceled]或
 *      [LambdaHolder#onDefined] 方法来设置 ResultCode 为 RESULT_CANCELED 或 RESULT_FIRST_USER 时的回调
 */
fun <F : FragmentActivity> F.startActivityForResult(
        intent: Intent,
        options: Bundle? = null,
        callback: (Intent) -> Unit = {}): LambdaHolder<Intent> {
    //获取一个与已有编码不重复的编码
    val requestCode = codeGenerate(resultHolder)
    //获取或创建Fragment
    val emptyFragment = findOrCreateEmptyFragment(supportFragmentManager)
    //调用Fragment的startActivityForResult方法,并传入回调
    return emptyFragment.startActivityForResult(requestCode, intent, options, callback)
}


/**
 * 申请权限的扩展方法,通过lambda传入回调,不需要重写[Activity#onRequestPermissionsResult]方法
 * @receiver F  基于[FragmentActivity]的扩展方法
 * @param permission Array<out String>  要申请的权限
 * @param onRequestDone () -> Unit  申请成功时的回调
 * @return LambdaHolder<Unit>   可以在此对象上继续调用[#onDenied]方法来设置申请失败时的回调
 */
fun <F : FragmentActivity> F.requestPermissions(
        vararg permission: String,
        onRequestDone: () -> Unit): LambdaHolder<Unit> {
    //获取一个与已有编码不重复的编码
    val requestCode = codeGenerate(permissionHolder)
    //查找Activity中有没有空的Fragment,如果没有则创建空的Fragment并添加到Activity中
    val emptyFragment = findOrCreateEmptyFragment(supportFragmentManager)
    //使用Fragment的requestPermissions方法申请权限
    //onRequestDone的类型是 () -> Unit,requestPermissions接收的类型是(Unit) -> Unit
    //所以不能直接传入,需要做个中转
    return emptyFragment.requestPermissions(requestCode, *permission) {
        onRequestDone()
    }
}

1.startActivityForResult方法就是最终调用的方法了,它是一个包级扩展方法,可以在FragmentActivity中直接调用,我们要传入的参数只有三个,其中options参数是可选的,没有动画需求的时候,可以忽略掉它。callback参数也是可选的,它作为接收返回成功时参数的回调,在部分情况下,也可能会不需要,此时往往要用到setBeforeCallback方法来设置一个总的回调,这个回调会在调用onDefined、onCanceled或onSuccess之前就会调用。

2.requestPermissions方法是申请权限时要调用的方法了,只需要传入一个可变的权限参数以及一个成功时的回调即可,后面还可以链式使用setOnDeniedCallback方法来设置失败时的回调,这个与上一个方法就简单得多了。

六、实践及效果展示

接下来,我通过调用相机拍照,然后将照片展示在界面上的这么一个简单功能,来展示这种写法到底是怎样的。

先创建一个Activity,布局长这样:

然后在res/xml下创建一个名为"file_paths.xml"的文件,输入以下内容:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path path="." name="sdcard" />
</paths>

再打开AndroidManifest.xml文件,在application节点内,添加以下代码:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.FileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>
</provider>

注意,因为我引入了AndroidX,所以android:name这一行是"androidx.core.content.FileProvider",没有引入AndroidX的话,请导入正确的FileProvider。

再添加要使用的摄像头权限:

<uses-permission android:name="android.permission.CAMERA"/>

我们打开Activity,添加一个toUri方法:

private fun File.toUri(): Uri {
    return if (Build.VERSION.SDK_INT > 23) {
        FileProvider.getUriForFile(this@MainActivity, "$packageName.FileProvider", this)     
    } else {
        Uri.fromFile(this)
    }
}

一个Activity内的扩展方法,与大多数人所理解的不太一样的是,它并不是一个静态的包级函数。

之后,为按钮设置点击事件,在onClick方法内添加以下代码:

override fun onClick(v: View?) {
        //先申请权限
        requestPermissions(Manifest.permission.CAMERA) {
            //把照片保存到不需要权限就能使用的缓存目录下
            val photoPath = externalCacheDir ?: cacheDir
            if (!photoPath.exists()) photoPath.mkdirs()
            //分配一个尽量不重复的名称
            val photoFile = File(photoPath, "${System.currentTimeMillis()}.jpg")
            if (photoFile.exists()) photoFile.delete()
            //转换为uri
            val outputUri = photoFile.toUri()
            //装载Intent
            val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                    .putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
            //启动Activity并接收结果
            startActivityForResult(intent) {
                //显示图片
                imageView.setImageURI(outputUri)
            }.setOnCanceledCallback {
                toast("取消了拍照")
            }
            //设置申请权限失败时的回调
        }.setOnDeniedCallback {
            AlertDialog.Builder(this)
                    .setTitle("提示")
                    .setCancelable(false)
                    .setMessage("摄像头权限被拒绝,无法调用相机功能。")
                    .setPositiveButton("我知道了") { d, _ ->
                        d.dismiss()
                    }
                    .setNegativeButton("再次申请") { d, _ ->
                        //直接调用此方法来继续发起申请权限操作
                        onClick(null)
                        d.dismiss()
                    }
                    .show()
        }
    }

以上就是基于Kotlin特性的使用方法了,可以看到,先申请了摄像头权限,后面还跟了一个setOnDeniedCallback方法传入了在申请权限失败时回调的Lambda,代码中是弹出对话框提示用户权限被拒绝了。当权限申请成功时,我们先是进行一堆设置照片路径的操作,然后调用了startActivityForResult方法,传入Intent对象以及成功时的Lambda回调,然后后面又跟了一个用于设置取消拍照时回调Lambda的setOnCanceledCallback方法,那么现在,我们来启动程序来看看效果如果。

申请权限时:

当权限被拒绝时:

当拍照取消时:

当拍照成功时:

七、使用inline特性实现仿Anko写法

新创建一个kt文件,输入以下代码:

/**
 * 启动Activity并接收Intent的扩展方法,不需要重写[#onActivityResult]方法
 * @receiver 基于[FragmentActivity]的扩展方法
 * @param params Array<out Pair<String, *>> 要携带的参数
 * @param options Bundle?   动画参数
 * @param callback (Intent) -> Unit 返回此界面时,当ResultCode为RESULT_OK时的回调
 * @return LambdaHolder<Intent> 可以在此对象上继续调用 [LambdaHolder#onCanceled]或
 *          [LambdaHolder#onDefined]方法来设置ResultCode为RESULT_CANCELED或RESULT_FIRST_USER时的回调
 */
inline fun <reified F : FragmentActivity> FragmentActivity.startActivityForResult(
        vararg params: Pair<String, *>,
        options: Bundle? = null,
        noinline callback: (Intent) -> Unit = {}): LambdaHolder<Intent> {
    return startActivityForResult(intentFor<F>(*params), options, callback)
}


/**
 * 基于[Fragment]的扩展方法
 */
inline fun <reified F : FragmentActivity> Fragment.startActivityForResult(
        vararg params: Pair<String, *>,
        options: Bundle? = null,
        noinline callback: (Intent) -> Unit = {}): LambdaHolder<Intent> {
    return requireActivity().startActivityForResult(intentFor<F>(*params), options, callback)
}

分别可以在FragmentActivity中以及Fragment中调用:

startActivityForResult<OtherActivity>("name" to "Kotlin", "size" to 1024) { intent ->
        toast("返回的数据:${intent.getStringExtra("name")}")
}.setOnCanceledCallback {
    toast("取消了操作")
}.setOnDefinedCallback {
    toast("自定义的动作")
}

或者是不管resultCode是什么都要执的操作:

startActivityForResult<OtherActivity>("name" to "Kotlin", "size" to 
        startActivityForResult<OtherActivity>().setBeforeCallback {
            toast("回来了,加载数据")
            //do something...
        }

所有代码看这里:

https://github.com/xiazunyang/ChainLambda

注意:本示例中,使用了AndroidX、Android Ktx以及Anko等扩展包、工具包,如需使用本篇中的代码,请按需调整代码。


推荐阅读:

神奇的Hook机制,一文读懂AOP编程

Android组件化最佳实践-ARetrofit

教你如何使用Flutter和原生App混合开发


欢迎关注我的公众号,学习技术或投稿

长按上图,识别图中二维码即可关注

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存